<!DOCTYPE html>
<html>
  <head>
    <title>
      Test Constructor: PeriodicWave
    </title>
    <script src="../../resources/testharness.js"></script>
    <script src="../../resources/testharnessreport.js"></script>
    <script src="../resources/audit-util.js"></script>
    <script src="../resources/audit.js"></script>
    <script src="../resources/audionodeoptions.js"></script>
  </head>
  <body>
    <script id="layout-test-code">
      let context;

      let audit = Audit.createTaskRunner();

      audit.define('initialize', (task, should) => {
        context = initializeContext(should);
        task.done();
      });

      audit.define('invalid constructor', (task, should) => {
        testInvalidConstructor(should, 'PeriodicWave', context);
        task.done();
      });

      audit.define('default constructor', (task, should) => {
        should(() => {
          node = new PeriodicWave(context);
        }, 'node = new PeriodicWave(context)').notThrow();

        task.done();
      });

      audit.define('constructor with options', (task, should) => {
        let node1;
        let options = {real: [1, 1]};
        should(
            () => {
              node1 = new PeriodicWave(context, options);
            },
            'node = new PeriodicWave(context, ' + JSON.stringify(options) + ')')
            .notThrow();
        should(node1 instanceof PeriodicWave, 'node1 instanceof PeriodicWave')
            .beEqualTo(true);

        let node2;
        options = {imag: [1, 1]};
        should(
            () => {
              node2 = new PeriodicWave(context, options);
            },
            'node2 = new PeriodicWave(context, ' + JSON.stringify(options) +
                ')')
            .notThrow();
        should(node2 instanceof PeriodicWave, 'node2 instanceof PeriodicWave')
            .beEqualTo(true);

        let node3;
        options = {real: [1, 2], imag: [1, 1]};
        should(
            () => {
              node3 = new PeriodicWave(context, options);
            },
            'node3 = new PeriodicWave(context, ' + JSON.stringify(options) +
                ')')
            .notThrow();
        should(node3 instanceof PeriodicWave, 'node3 instanceof PeriodicWave')
            .beEqualTo(true);

        task.done();
      });

      // The following test that the correct waveforms are produced when various
      // possible PeriodicWave options are used.  These are needed because it's
      // the only way to tell if the various options were correctly applied.

      // TODO(rtoy): These functionality tests should be moved out to a separate
      // file.
      audit.define('1: real periodicwave test', (task, should) => {
        verifyPeriodicWaveOutput(
            should, {real: [0, 2]}, generateReference(Math.cos), 2.7143e-5)
            .then(() => task.done());
      });

      audit.define('2: real periodicwave test', (task, should) => {
        verifyPeriodicWaveOutput(
            should, {real: [0, 2], disableNormalization: false},
            generateReference(Math.cos), 2.7143e-5)
            .then(() => task.done());
      });

      audit.define('3: real periodicwave test', (task, should) => {
        verifyPeriodicWaveOutput(
            should, {real: [0, 2], disableNormalization: true},
            generateReference(x => 2 * Math.cos(x)), 5.4285e-5)
            .then(() => task.done());
      });

      audit.define('1: imag periodicwave test', (task, should) => {
        verifyPeriodicWaveOutput(
            should, {imag: [0, 2]}, generateReference(Math.sin), 2.7262e-5)
            .then(() => task.done());
      });

      audit.define('2: imag periodicwave test', (task, should) => {
        verifyPeriodicWaveOutput(
            should, {imag: [0, 2], disableNormalization: false},
            generateReference(Math.sin), 2.7262e-5)
            .then(() => task.done());
      });

      audit.define('3: imag periodicwave test', (task, should) => {
        verifyPeriodicWaveOutput(
            should, {imag: [0, 2], disableNormalization: true},
            generateReference(x => 2 * Math.sin(x)), 5.4524-5)
            .then(() => task.done());
      });

      audit.define('1: real/imag periodicwave test', (task, should) => {
        verifyPeriodicWaveOutput(
            should, {
              real: [0, 1],
              imag: [0, 1],
            },
            generateReference(x => Math.SQRT1_2 * (Math.sin(x) + Math.cos(x))),
            3.8371e-5)
            .then(() => task.done());
      });

      audit.define('2: real/imag periodicwave test', (task, should) => {
        verifyPeriodicWaveOutput(
            should, {real: [0, 1], imag: [0, 1], disableNormalization: false},
            generateReference(x => Math.SQRT1_2 * (Math.sin(x) + Math.cos(x))),
            2.7225e-5)
            .then(() => task.done());
      });

      audit.define('3: real/imag periodicwave test', (task, should) => {
        verifyPeriodicWaveOutput(
            should, {real: [0, 1], imag: [0, 1], disableNormalization: true},
            generateReference(x => Math.sin(x) + Math.cos(x)), 3.8501e-5)
            .then(() => task.done());
      });

      // Returns a function that generates the expected reference array where
      // the samples are generated by the function |gen|.
      function generateReference(gen) {
        return (length, freq, sampleRate) => {
          let expected = new Float32Array(length);
          let omega = 2 * Math.PI * freq / sampleRate;
          for (let k = 0; k < length; ++k) {
            expected[k] = gen(omega * k);
          }
          return expected;
        };
      }

      // Verify that an oscillator constructed from the given periodic wave
      // produces the expected result.
      function verifyPeriodicWaveOutput(
          should, waveOptions, expectedFunction, threshold) {
        let node;
        // Rather arbitrary sample rate and render length. Length doesn't have
        // to be very long.
        let sampleRate = 48000;
        let renderLength = 0.25;
        let testContext =
            new OfflineAudioContext(1, renderLength * sampleRate, sampleRate);

        let options = {
          periodicWave: new PeriodicWave(testContext, waveOptions)
        };
        node = new OscillatorNode(testContext, options);

        // Create the graph
        node.connect(testContext.destination);
        node.start();

        return testContext.startRendering().then(function(resultBuffer) {
          let actual = resultBuffer.getChannelData(0);
          let expected = expectedFunction(
              actual.length, node.frequency.value, testContext.sampleRate);
          // Actual must match expected to within the (experimentally)
          // determined threshold.
          let message = '';
          if (waveOptions.disableNormalization != undefined)
            message =
                'disableNormalization: ' + waveOptions.disableNormalization;
          if (waveOptions.real) {
            if (message.length > 0)
              message += ', '
              message += 'real: [' + waveOptions.real + ']';
          }
          if (waveOptions.imag) {
            if (message.length > 0)
              message += ', '
              message += 'imag: [' + waveOptions.imag + ']';
          }
          should(actual, 'Oscillator with periodicWave {' + message + '}')
              .beCloseToArray(expected, {absoluteThreshold: threshold});
        });
      }

      // Verify that the default PeriodicWave produces a sine wave. Use a
      // 2-channel context to verify this.
      function sineWaveTest(should, waveFun, message) {
        // Channel 0 is the output from the PeriodicWave, and channel 1 is the
        // reference oscillator output.
        let context = new OfflineAudioContext(2, 40000, 40000);
        let oscRef =
            new OscillatorNode(context, {type: 'sine', frequency: 440});
        let wave = waveFun(context);
        let oscTest =
            new OscillatorNode(context, {frequency: 440, periodicWave: wave});

        let merger = new ChannelMergerNode(context, {numberOfInputs: 2});

        oscRef.connect(merger, 0, 1);
        oscTest.connect(merger, 0, 0);

        merger.connect(context.destination);

        oscRef.start();
        oscTest.start();

        return context.startRendering().then(output => {
          // The output from the two channels MUST match exactly.
          let actual = output.getChannelData(0);
          let ref = output.getChannelData(1);

          should(actual, message).beEqualToArray(ref);
        });
      }

      audit.define('default wave', (task, should) => {
        // Verify that the default PeriodicWave produces a sine wave.
        sineWaveTest(
            should, (context) => new PeriodicWave(context),
            'new PeriodicWave(context) output')
            .then(() => task.done());
      });

      audit.define('default wave (with dict)', (task, should) => {
        // Verify that the default PeriodicWave produces a sine wave when the
        // PeriodicWaveOptions dictionary is given, but real and imag members
        // are not set.
        sineWaveTest(
            should, (context) => new PeriodicWave(context, {foo: 42}),
            'new PeriodicWave(context, {foo: 42}) output')
            .then(() => task.done());
      });

      audit.run();
    </script>
  </body>
</html>
